using System;
using System.Drawing;
using System.Windows.Forms;
using Microsoft.DirectX;
using Microsoft.DirectX.Direct3D;

namespace MetaWorld
{



	/// <summary>
	/// This class sets up and encapsulates the DirectX Device class. 
	/// The main difference is in a number of static properties 
	/// that can be set before the device itself is initialized.
	/// </summary>
	public class Scene
	{

		/// <summary>
		/// Reference to the window which will display the scene.
		/// </summary>
		public Form Window;

		/// <summary>
		/// Internal: Direct3D Device object. 
		/// </summary>
		protected Device device;

		/// <summary>
		/// Accessor for the DirectX device object.
		/// </summary>
		public Device Device { get { return device; } }
		



		/// <summary>
		/// Internal: camera setup variable. 
		/// </summary>
		protected Vector3 cameraPosition, cameraOrientation, cameraTarget;

		/// <summary>
		/// Internal: camera setup variable. 
		/// </summary>
		protected float cameraFieldOfView, cameraAspectRatio;

		/// <summary>
		/// Scene background color
		/// </summary>
		public Color BackgroundColor;

		/// <summary>
		/// Internal: time when the last frame was drawn
		/// </summary>
		protected DateTime frameTime;

		/// <summary>
		/// Internal: time span between frames
		/// </summary>
		protected TimeSpan frameDelta;

		/// <summary>
		/// Internal: average frames per second measure
		/// </summary>
		protected double FPS;

		/// <summary>
		/// Internal: is programmable pipeline enabled?
		/// </summary>
		protected bool canDoShaders;

		/// <summary>
		/// Returns true if programmable pixel shader can be used
		/// on current hardware.
		/// </summary>
		public bool IsPixelShadingEnabled { get { return canDoShaders; } }

		/* CAMERA CODE */


		/// <summary>
		/// Camera's up-vector, in left-hand coordinates.
		/// </summary>
		public Vector3 CameraOrientation 
		{
			get 
			{ 
				return cameraOrientation; 
			}
			set 
			{
				this.cameraOrientation = value; 
				RefreshCamera ();
			}
		}

		/// <summary>
		/// Camera position, in left-hand coordinates.
		/// </summary>
		public Vector3 CameraPosition 
		{
			get 
			{
				return cameraPosition;
			}
			set 
			{
				this.cameraPosition = value;
				RefreshCamera ();
			}
		}

		/// <summary>
		/// Sets up the camera target (i.e. point to look at, in left-hand coordinates).
		/// </summary>
		public Vector3 CameraTarget
		{
			get 
			{
				return cameraTarget; 
			}
			set 
			{ 
				this.cameraTarget = value;
				RefreshCamera ();
			}
		}

		/// <summary>
		/// Field of view in radians. PI/4 is a common value.
		/// </summary>
		public float CameraFieldOfView 
		{
			get 
			{
				return this.cameraFieldOfView; 
			}
			set 
			{
				this.cameraFieldOfView = value;
				RefreshCamera ();
			}
		}

		/// <summary>
		/// Camera's aspect ratio based on dimensions of the display window.
		/// </summary>
		public float CameraAspectRatio
		{
			get 
			{
				return this.cameraAspectRatio; 
			}
			set 
			{
				this.cameraAspectRatio = value;
				RefreshCamera ();
			}
		}

		/// <summary>
		/// Internal: Set up the camera transform vector, based on the SetCamera...() functions.
		/// </summary>
		protected void RefreshCamera ()
		{
			device.Transform.Projection = 
				Matrix.PerspectiveFovLH (this.cameraFieldOfView, this.cameraAspectRatio, 1f, 100f);
			device.Transform.View = 
				Matrix.LookAtLH (this.cameraPosition, this.cameraTarget, this.cameraOrientation);
		}


		/* LIGHTING CODE */


		/// <summary>
		/// Get the total number of lights supported by this system;
		/// they are zero-indexed, so the valid light indices are in [0, total - 1].
		/// </summary>
		public int MaxLightsAllowed
		{
			get { return device.Lights.Count; }
		}

		/// <summary>
		/// Turns the specified light on.
		/// </summary>
		/// <param name="index"></param>
		public void TurnLightOn (int index)
		{
			device.Lights[index].Enabled = true;
		}

		/// <summary>
		/// Turns the specified light off.
		/// </summary>
		/// <param name="index"></param>
		public void TurnLightOff (int index)
		{
			device.Lights[index].Enabled = false;
		}

		/// <summary>
		/// Specifies the given light to be a 'light bulb', emitting 
		/// light equally in all directions from the given point in space.
		/// This does not turn the light on (use the TurnLightOn method for that).
		/// </summary>
		/// <param name="index"></param>
		/// <param name="x"></param>
		/// <param name="y"></param>
		/// <param name="z"></param>
		public void SetLightAsPoint (int index, float x, float y, float z)
		{
			SetLightAsPoint (index, x, y, z, 0.7f, 1000f);
		}
		
		/// <summary>
		/// Specifies the given light to be a 'light bulb', emitting
		/// light equally in all directions from the given point in space.
		/// This does not turn the light on (use the TurnLightOn method for that).
		/// </summary>
		/// <param name="index"></param>
		/// <param name="x"></param>
		/// <param name="y"></param>
		/// <param name="z"></param>
		/// <param name="attenuation"></param>
		/// <param name="range"></param>
		public void SetLightAsPoint (int index, float x, float y, float z,
			float attenuation, float range)
		{
			device.Lights[index].Type = LightType.Point;
			device.Lights[index].Position = new Vector3 (x, y, z);
			device.Lights[index].Range = range;
			device.Lights[index].Attenuation0 = attenuation;
			device.Lights[index].Update ();
		}



		// TODO: add spotlight functionality
		// public void SetupSpotLight (int index, ...)

		/// <summary>
		/// Specifies the given light as coming from infinite distance at 
		/// a given angle - good for modeling natural light.
		/// This does not turn the light on (use the TurnLightOn method for that).
		/// </summary>
		/// <param name="index"></param>
		/// <param name="x"></param>
		/// <param name="y"></param>
		/// <param name="z"></param>
		public void SetLightAsDirectional (int index, float x, float y, float z)
		{
			device.Lights[index].Type = LightType.Directional;
			device.Lights[index].Direction = new Vector3 (x, y, z);
			device.Lights[index].Update ();
		}

		/// <summary>
		/// Sets the ambient and diffuse light colors for the given light.
		/// Diffuse color is the color applied to surfaces facing the light.
		/// Ambient color is the 'background' color, applied even to surfaces
		/// not facing the light.
		/// </summary>
		/// <param name="index"></param>
		/// <param name="ambient"></param>
		/// <param name="diffuse"></param>
		public void SetLightColor (int index, Color diffuse, Color ambient)
		{
			device.Lights[index].Ambient = ambient;
			device.Lights[index].Diffuse = diffuse;
			device.Lights[index].Update ();
		}


		/// <summary>
		/// Internal: initializes member variables.
		/// </summary>
		protected void InitializeMemberVariables ()
		{
			this.cameraOrientation = new Vector3 (0f, 0f, 0f);
			this.cameraPosition = new Vector3 (0f, 0f, 0f);
			this.cameraTarget = new Vector3 (0f, 0f, 0f);

			this.BackgroundColor = Color.CornflowerBlue;
			this.FPS = 30;
		}



		/// <summary>
		/// Constructor. Creates a scene inside a window with given parameters.
		/// </summary>
		/// <param name="width"></param>
		/// <param name="height"></param>
		/// <param name="root"></param>
		public Scene(int width, int height, SceneGraphNode root)
		{
			InitializeMemberVariables ();

			Window = new Form ();
			Window.Width = width;
			Window.Height = height;
			Window.Text = "MetaWorld";
			Window.Show ();

			PresentParameters pars = new PresentParameters ();
			pars.SwapEffect = SwapEffect.Discard;
			pars.Windowed = true;
			pars.EnableAutoDepthStencil = true;
			pars.AutoDepthStencilFormat = DepthFormat.D16;
			
			// Initialize the programmable pipeline, if available
			canDoShaders = true;
			Caps hardware = Manager.GetDeviceCaps (0, DeviceType.Hardware);

			if ((hardware.VertexShaderVersion >= root.VertexShaderRequiredVersion) &&
				(hardware.PixelShaderVersion >= root.PixelShaderRequiredVersion))
			{
				// Default to software processing
				CreateFlags flags = CreateFlags.SoftwareVertexProcessing;

				// But use hardware if available
				if (hardware.DeviceCaps.SupportsHardwareTransformAndLight)
					flags = CreateFlags.HardwareVertexProcessing;

				// And use pure if available
				//if (hardware.DeviceCaps.SupportsPureDevice)
				//	flags |= CreateFlags.PureDevice;

				device = new Device (0, DeviceType.Hardware, Window, flags, pars);
			}
			else
			{
				// No shader support
				canDoShaders = false;

				device = new Device (0, DeviceType.Reference, Window, 
					CreateFlags.SoftwareVertexProcessing, pars);
			}

			// set up additional properties here...
			// device.RenderState.Lighting = false;
			// device.RenderState.Ambient = Color.Red;
			// device.RenderState.CullMode = Cull.None;

			SetUpDefaults (height, width);
		}


		/// <summary>
		/// Sets up default camera and light
		/// </summary>
		protected void SetUpDefaults (float height, float width)
		{
			// set up the default camera
			CameraPosition = new Vector3 (0f, 0f, -10f);   // sit a wee bit behind the origin
			CameraTarget = new Vector3 (0f, 0f, 0f);       // look at the origin 
			CameraOrientation = new Vector3 (0f, 1f, 0f);  // up is along the y-axis
			CameraFieldOfView = (float) (Math.PI / 4);
			CameraAspectRatio = height / width;

			// set up the default light
			SetLightAsPoint (0, 10f, 10f, -10f); // to the right and above the camera
			SetLightColor (0, Color.White, Color.FromArgb (16, 16, 16));
			TurnLightOn (0);

			device.Material = new Material ();
		}

		/// <summary>
		/// Call this function to clear the screen and prepare for drawing objects.
		/// </summary>
		public void BeginRendering ()
		{
			device.Clear (ClearFlags.Target | ClearFlags.ZBuffer, 
				this.BackgroundColor, 1f, 0);
			device.BeginScene ();
			ResetWorldTransform ();
		}


		/// <summary>
		/// Clears the world transform matrix - use before rendering each object...
		/// </summary>
		public void ResetWorldTransform ()
		{
			device.Transform.World = Matrix.Identity;
		}

		/// <summary>
		/// Call this function to finalize drawing and display the scene in the window.
		/// </summary>
		public void EndRendering ()
		{
			device.EndScene ();
			device.Present ();
			DateTime t = DateTime.Now;
			frameDelta = t - frameTime;
			frameTime = t;
		}


		/// <summary>
		/// Returns the approximate frames per second rendering rate for the scene
		/// </summary>
		/// <returns></returns>
		public double FramesPerSecond
		{ 
			get 
			{	
				double CurrentFPS = 1.0 / frameDelta.TotalSeconds;
                if (Double.IsInfinity (CurrentFPS))	CurrentFPS = 100;
				FPS = (FPS * 29 + CurrentFPS) / 30;
				return FPS;
			}
		}


		/// <summary>
		/// Helper function. Converts screen coordinates (x, y) into a point 
		/// on the intersection plane.
		/// </summary>
		/// <param name="x"></param>
		/// <param name="y"></param>
		/// <param name="intersection"></param>
		/// <returns></returns>
		public Vector3 ScreenToWorld (double x, double y, Plane intersection)
		{
			Vector3 close = Vector3.Unproject (new Vector3 ((float) x, (float) y, 0f),
				this.Device.Viewport,
				this.Device.Transform.Projection,
				this.Device.Transform.View,
				this.Device.Transform.World);
			Vector3 far = Vector3.Unproject (new Vector3 ((float) x, (float) y, 1f),
				this.Device.Viewport,
				this.Device.Transform.Projection,
				this.Device.Transform.View,
				this.Device.Transform.World);
			return Plane.IntersectLine (intersection, close, far);
		}

		/// <summary>
		/// Helper function. Converts screen coordinates (x, y) into a point
		/// on a plane perpendicular to the viewer, passing through the origin.
		/// </summary>
		/// <param name="x"></param>
		/// <param name="y"></param>
		/// <returns></returns>
		public Vector3 ScreenToWorld (double x, double y)
		{
			Plane p = Plane.FromPointNormal (this.cameraTarget, this.cameraTarget - this.cameraPosition);
			return ScreenToWorld (x, y, p);
		}



	}
}
